package com.googlecode.uipipedream.spec;

import java.awt.Color;
import java.util.Observable;

/**
 * <p>
 * Represents an aproximation of a colour selection.
 * </p>
 * 
 * <p>
 * This file is part of UIPipeDream.
 * </p>
 * 
 * <p>
 * You can redistribute UIPipeDream and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation, either
 * version 3 of the License, or (at your option) any later version.
 * </p>
 * 
 * <p>
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * </p>
 * 
 * <p>
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <a
 * href="http://www.gnu.org/licenses/">http://www.gnu.org/licenses/</a>.
 * </p>
 * 
 * @author <a href="http://daemonsadvocate.blogspot.com/">Matthew B. Gray</a>
 * @author James Jones
 * @version 1.0 Final
 */
public class ColourModel extends Observable {
	private PalletSelector type = PalletSelector.ANALOGUS;
	public PalletSelector getType() { return type; }
	public void setType(PalletSelector type) { this.type = type; }

	// Outrigger variable declarations.
	private float offset = 1f/15; // scalar
	private int nColours = 3;
	
	// Primary colour variable declarations.
	private float brightness = (float) 1.0;
	private float hue = (float) 0.0;
	private float saturation = (float) 0.0;
	
	// Outrigger controls.
	private final float SMALLEST_OFFSET = 0.05f;
	public float getOffset() { return offset; }
	public int getNColours() { return nColours; }
	
	public void setOffset(float newOffset) {
		float max = 1f/this.nColours;
		// Normalize inputs.
		if(max < newOffset)
			newOffset = max;
		else if(newOffset < SMALLEST_OFFSET)
			newOffset = SMALLEST_OFFSET;
		
		this.offset = newOffset;
		setChanged();
	}
	
	public void setNColours(int colours) {
		// Normalize inputs.
		if(colours%2==0)
			colours = colours - 1;
		if(colours<1)
			colours = 1;
		
		// Update offset.
		float max = 1f/colours;
		if(max < this.offset)
			this.offset = max;
		
		// Update colours.
		this.nColours = colours;
		setChanged();
	}
	
	// Primary colour controls.
	public void setColor(Color c) { setRGB(c.getRed(), c.getGreen(), c.getBlue()); }
	public Color getColor() { return Color.getHSBColor(hue, saturation, brightness); }
	public float getHue() { return hue; }
	public float getSaturation() { return saturation; }
	public float getBrightness() { return brightness; }
	
	public void setBrightness(float brightness) {
		if (brightness < 0.0) brightness = 0f;
		if (brightness >= 1.0) brightness = 1f;
		this.brightness = brightness;
		setChanged();
	}
	
	public void setHue(float hue) {
		if (hue < 0.0)
			hue = 1f+hue;
		this.hue = hue % 1f;
		setChanged();
	}
	
	public void setSaturation(float saturation) {
		if (saturation < 0.0) saturation = 0f;
		if (saturation >= 1.0) saturation = 1f;
		this.saturation = saturation;
		setChanged();
	}
	
	public void setRGB(int r, int g, int b) {
		if (r < 0) r = 0;
		if (r > 255) r = 255;
		if (g < 0) g = 0;
		if (g > 255) g = 255;
		if (b < 0) b = 0;
		if (b > 255) b = 255;
		float[] vals = new float[3];
		Color.RGBtoHSB(r, g, b, vals);
		this.hue = vals[0];
		this.saturation = vals[1];
		this.brightness = vals[2];
		setChanged();
	}
	
	public float getComplementaryHue() { return (0.5f+hue) % 1f; }
	
	public float[] getAnalogusHues() {
		final float[] toReturn = new float[nColours];
		final int halfN = nColours/2;
		
		float leftOutrigger, incrementalOffset, rightOutrigger;
		for (int i=halfN; 0<i; i--) {
			// 1. Setup internal stuff.
			incrementalOffset = offset * i;
			leftOutrigger = hue - incrementalOffset;
			if(leftOutrigger<0f)
				leftOutrigger += 1f;
			rightOutrigger = (hue + incrementalOffset) % 1f;
			
			// 2. Set returning values.
			toReturn[halfN - i] = leftOutrigger;
			toReturn[halfN + i] = rightOutrigger;
		}
		toReturn[halfN] = hue;
		return toReturn;
	}
	
	public float[] getSplitComplementHues() {
		final float[] toReturn = new float[nColours-1];
		final int halfN = nColours/2;
		
		for (int i=halfN; 0<i; i--) {
			// 1. Setup internal stuff.
			final float incrementalOffset = offset * i;
			final float leftOutrigger = (this.getComplementaryHue() - incrementalOffset) % 1f;
			final float rightOutrigger = (this.getComplementaryHue() + incrementalOffset) % 1f;
			
			// 2. Set returning values.
			toReturn[halfN - i] = leftOutrigger;
			toReturn[halfN + i - 1] = rightOutrigger;
		}
		return toReturn;
	}
	
	protected void setChanged() {
		super.setChanged();
		notifyObservers();
	}
	
	// Outrigger Brainstorm.
	/**@deprecated*/ public float getSplitComplementaryHue1() { return (getComplementaryHue() + offset) % 1f; }
	/**@deprecated*/ public float getSplitComplementaryHue2() { return (getComplementaryHue() - offset) % 1f; }
	/**@deprecated*/ public float getTriadHue1() { return (1f/3 + hue) % 1f; }
	/**@deprecated*/ public float getTriadHue2() { return ((2f/3) + hue) % 1f; }
	/**@deprecated*/ public float getAnalogHue1() { return (hue + offset) % 1f; }
	/**@deprecated*/ public float getAnalogHue2() { return (hue - offset) % 1f; }
	
	@Override
	public ColourModel clone() {
		ColourModel that = new ColourModel();
		that.brightness = this.brightness;
		that.hue = this.hue;
		that.saturation = this.saturation;
		that.nColours = this.nColours;
		that.offset = this.offset;
		return that;
	}
	
	/**
	 * Rather than reloading all observers with new models to look at, thos method
	 * allows restoration from another model onto the current one and notifies observers
	 * to repaint.
	 * @param that A valid colourmodel object stored somewhere else that we're copying the
	 * values from.
	 */
	public void hydrate(ColourModel that) {
		this.brightness = that.brightness;
		this.hue = that.hue;
		this.saturation = that.saturation;
		this.nColours = that.nColours;
		this.offset = that.offset;
		notifyObservers();
	}
	
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + Float.floatToIntBits(brightness);
		result = prime * result + Float.floatToIntBits(hue);
		result = prime * result + nColours;
		result = prime * result + Float.floatToIntBits(offset);
		result = prime * result + Float.floatToIntBits(saturation);
		return result;
	}
	
	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ColourModel other = (ColourModel) obj;
		if (Float.floatToIntBits(brightness) != Float
				.floatToIntBits(other.brightness))
			return false;
		if (Float.floatToIntBits(hue) != Float.floatToIntBits(other.hue))
			return false;
		if (nColours != other.nColours)
			return false;
		if (Float.floatToIntBits(offset) != Float.floatToIntBits(other.offset))
			return false;
		if (Float.floatToIntBits(saturation) != Float
				.floatToIntBits(other.saturation))
			return false;
		return true;
	}
	
	public Color[] exportAnalogusColours() {
		float[] hues = this.getAnalogusHues();
		Color[] toReturn = new Color[hues.length];
		
		for (int i = 0; i < hues.length; i++)
			toReturn[i] = Color.getHSBColor(
					hues[i], this.getSaturation(), this.getBrightness());
		
		return toReturn;
	}
	
	public Color[] exportSplitComplementaryColours() {
		float[] hues = this.getSplitComplementHues();
		Color[] toReturn = new Color[hues.length+1];
		
		for (int i = 0; i < hues.length; i++)
			toReturn[i] = Color.getHSBColor(
					hues[i], this.getSaturation(), this.getBrightness());
		
		toReturn[toReturn.length-1] = this.getColor();
		return toReturn;
	}

	public Color[] exportComplementaryColour() {
		return new Color[] {
				Color.getHSBColor(this.hue, this.saturation, this.brightness),
				Color.getHSBColor(this.getComplementaryHue(), this.saturation, this.brightness)
		};
	}
}